Completed
Pull Request — master (#15)
by
unknown
08:34 queued 13s
created

WaveFileCreator.createExtensibleHeader_   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 13
rs 9.9
c 0
b 0
f 0
cc 1
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileCreator class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import WaveFileParser from './wavefile-parser';
33
import interleave from './interleave';
34
import dwChannelMask from './dw-channel-mask';
35
import validateNumChannels from './validate-num-channels'; 
36
import validateSampleRate from './validate-sample-rate';
37
import {packArrayTo, packTo, unpack, unpackArray} from 'byte-data';
38
39
/**
40
 * A class to read, write and create wav files.
41
 * @extends WaveFileParser
42
 */
43
export default class WaveFileCreator extends WaveFileParser {
44
45
  constructor() {
46
    super();
47
    /**
48
     * The bit depth code according to the samples.
49
     * @type {string}
50
     */
51
    this.bitDepth = '0';
52
    /**
53
     * @type {!Object}
54
     * @protected
55
     */
56
    this.dataType = {};
57
    /**
58
     * Audio formats.
59
     * Formats not listed here should be set to 65534,
60
     * the code for WAVE_FORMAT_EXTENSIBLE
61
     * @enum {number}
62
     * @protected
63
     */
64
    this.WAV_AUDIO_FORMATS = {
65
      '4': 17,
66
      '8': 1,
67
      '8a': 6,
68
      '8m': 7,
69
      '16': 1,
70
      '24': 1,
71
      '32': 1,
72
      '32f': 3,
73
      '64': 3
74
    };
75
  }
76
77
  /**
78
   * Set up the WaveFileCreator object based on the arguments passed.
79
   * Existing chunks are reset.
80
   * @param {number} numChannels The number of channels
81
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
82
   * @param {number} sampleRate The sample rate.
83
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
84
   * @param {string} bitDepthCode The audio bit depth code.
85
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
86
   *    or any value between '8' and '32' (like '12').
87
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
88
   *    The samples. Must be in the correct range according to the bit depth.
89
   * @param {?Object} options Optional. Used to force the container
90
   *    as RIFX with {'container': 'RIFX'}
91
   * @throws {Error} If any argument does not meet the criteria.
92
   */
93
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
94
    // reset all chunks
95
    this.clearHeaders();
96
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
97
  }
98
99
  /**
100
   * Set up the WaveFileCreator object based on the arguments passed.
101
   * @param {number} numChannels The number of channels
102
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
103
   * @param {number} sampleRate The sample rate.
104
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
105
   * @param {string} bitDepthCode The audio bit depth code.
106
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
107
   *    or any value between '8' and '32' (like '12').
108
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
109
   *    The samples. Must be in the correct range according to the bit depth.
110
   * @param {?Object} options Optional. Used to force the container
111
   *    as RIFX with {'container': 'RIFX'}
112
   * @throws {Error} If any argument does not meet the criteria.
113
   * @private
114
   */
115
  newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
116
    if (!options.container) {
117
      options.container = 'RIFF';
118
    }
119
    this.container = options.container;
120
    this.bitDepth = bitDepthCode;
121
    samples = interleave(samples);
122
    this.updateDataType_();
123
    /** @type {number} */
124
    let numBytes = this.dataType.bits / 8;
125
    this.data.samples = new Uint8Array(samples.length * numBytes);
126
    packArrayTo(samples, this.dataType, this.data.samples);
127
    this.makeWavHeader_(
128
      bitDepthCode, numChannels, sampleRate,
129
      numBytes, this.data.samples.length, options);
130
    this.data.chunkId = 'data';
131
    this.data.chunkSize = this.data.samples.length;
132
    this.validateWavHeader_();
133
  }
134
135
  /**
136
   * Set up the WaveFileParser object from a byte buffer.
137
   * @param {!Uint8Array} wavBuffer The buffer.
138
   * @param {boolean=} samples True if the samples should be loaded.
139
   * @throws {Error} If container is not RIFF, RIFX or RF64.
140
   * @throws {Error} If format is not WAVE.
141
   * @throws {Error} If no 'fmt ' chunk is found.
142
   * @throws {Error} If no 'data' chunk is found.
143
   */
144
  fromBuffer(wavBuffer, samples=true) {
145
    super.fromBuffer(wavBuffer, samples);
146
    this.bitDepthFromFmt_();
147
    this.updateDataType_();
148
  }
149
150
  /**
151
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
152
   * The return value of this method can be written straight to disk.
153
   * @return {!Uint8Array} A wav file.
154
   * @throws {Error} If bit depth is invalid.
155
   * @throws {Error} If the number of channels is invalid.
156
   * @throws {Error} If the sample rate is invalid.
157
   */
158
  toBuffer() {
159
    this.validateWavHeader_();
160
    return super.toBuffer();
161
  }
162
163
  /**
164
   * Return the sample at a given index.
165
   * @param {number} index The sample index.
166
   * @return {number} The sample.
167
   * @throws {Error} If the sample index is off range.
168
   */
169
  getSample(index) {
170
    index = index * (this.dataType.bits / 8);
171
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
172
      throw new Error('Range error');
173
    }
174
    return unpack(
175
      this.data.samples.slice(index, index + this.dataType.bits / 8),
176
      this.dataType);
177
  }
178
179
    /**
180
     * Return the sample at a given index.
181
     * @param {number} startIndex The sample start index.
182
     * @param {number} stopIndex The sample stop index.
183
     * @return {number} The sample.
184
     * @throws {Error} If the sample index is off range.
185
     */
186
    getSamples(startIndex, stopIndex) {
187
        startIndex = startIndex * (this.dataType.bits / 8);
188
        stopIndex = stopIndex * (this.dataType.bits / 8);
189
        if (stopIndex + this.dataType.bits / 8 > this.data.samples.length) {
190
            throw new Error('Range error');
191
        }
192
        return unpackArray(
193
            this.data.samples.slice(startIndex, stopIndex),
194
            this.dataType
195
        );
196
    }
197
198
  /**
199
   * Set the sample at a given index.
200
   * @param {number} index The sample index.
201
   * @param {number} sample The sample.
202
   * @throws {Error} If the sample index is off range.
203
   */
204
  setSample(index, sample) {
205
    index = index * (this.dataType.bits / 8);
206
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
207
      throw new Error('Range error');
208
    }
209
    packTo(sample, this.dataType, this.data.samples, index);
210
  }
211
212
  /**
213
   * Define the header of a wav file.
214
   * @param {string} bitDepthCode The audio bit depth
215
   * @param {number} numChannels The number of channels
216
   * @param {number} sampleRate The sample rate.
217
   * @param {number} numBytes The number of bytes each sample use.
218
   * @param {number} samplesLength The length of the samples in bytes.
219
   * @param {!Object} options The extra options, like container defintion.
220
   * @private
221
   */
222
  makeWavHeader_(
223
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
224
    if (bitDepthCode == '4') {
225
      this.createADPCMHeader_(
226
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
227
228
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
229
      this.createALawMulawHeader_(
230
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
231
232
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
233
        numChannels > 2) {
234
      this.createExtensibleHeader_(
235
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
236
237
    } else {
238
      this.createPCMHeader_(
239
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
240
    }
241
  }
242
243
  /**
244
   * Create the header of a linear PCM wave file.
245
   * @param {string} bitDepthCode The audio bit depth
246
   * @param {number} numChannels The number of channels
247
   * @param {number} sampleRate The sample rate.
248
   * @param {number} numBytes The number of bytes each sample use.
249
   * @param {number} samplesLength The length of the samples in bytes.
250
   * @param {!Object} options The extra options, like container defintion.
251
   * @private
252
   */
253
  createPCMHeader_(
254
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
255
    this.container = options.container;
256
    this.chunkSize = 36 + samplesLength;
257
    this.format = 'WAVE';
258
    this.bitDepth = bitDepthCode;
259
    this.fmt = {
260
      chunkId: 'fmt ',
261
      chunkSize: 16,
262
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
263
      numChannels: numChannels,
264
      sampleRate: sampleRate,
265
      byteRate: (numChannels * numBytes) * sampleRate,
266
      blockAlign: numChannels * numBytes,
267
      bitsPerSample: parseInt(bitDepthCode, 10),
268
      cbSize: 0,
269
      validBitsPerSample: 0,
270
      dwChannelMask: 0,
271
      subformat: []
272
    };
273
  }
274
275
  /**
276
   * Create the header of a ADPCM wave file.
277
   * @param {string} bitDepthCode The audio bit depth
278
   * @param {number} numChannels The number of channels
279
   * @param {number} sampleRate The sample rate.
280
   * @param {number} numBytes The number of bytes each sample use.
281
   * @param {number} samplesLength The length of the samples in bytes.
282
   * @param {!Object} options The extra options, like container defintion.
283
   * @private
284
   */
285
  createADPCMHeader_(
286
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
287
    this.createPCMHeader_(
288
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
289
    this.chunkSize = 40 + samplesLength;
290
    this.fmt.chunkSize = 20;
291
    this.fmt.byteRate = 4055;
292
    this.fmt.blockAlign = 256;
293
    this.fmt.bitsPerSample = 4;
294
    this.fmt.cbSize = 2;
295
    this.fmt.validBitsPerSample = 505;
296
    this.fact = {
297
      chunkId: 'fact',
298
      chunkSize: 4,
299
      dwSampleLength: samplesLength * 2
300
    };
301
  }
302
303
  /**
304
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
305
   * @param {string} bitDepthCode The audio bit depth
306
   * @param {number} numChannels The number of channels
307
   * @param {number} sampleRate The sample rate.
308
   * @param {number} numBytes The number of bytes each sample use.
309
   * @param {number} samplesLength The length of the samples in bytes.
310
   * @param {!Object} options The extra options, like container defintion.
311
   * @private
312
   */
313
  createExtensibleHeader_(
314
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
315
    this.createPCMHeader_(
316
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
317
    this.chunkSize = 36 + 24 + samplesLength;
318
    this.fmt.chunkSize = 40;
319
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
320
    this.fmt.cbSize = 22;
321
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
322
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
323
    // subformat 128-bit GUID as 4 32-bit values
324
    // only supports uncompressed integer PCM samples
325
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
326
  }
327
328
  /**
329
   * Create the header of mu-Law and A-Law wave files.
330
   * @param {string} bitDepthCode The audio bit depth
331
   * @param {number} numChannels The number of channels
332
   * @param {number} sampleRate The sample rate.
333
   * @param {number} numBytes The number of bytes each sample use.
334
   * @param {number} samplesLength The length of the samples in bytes.
335
   * @param {!Object} options The extra options, like container defintion.
336
   * @private
337
   */
338
  createALawMulawHeader_(
339
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
340
    this.createPCMHeader_(
341
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
342
    this.chunkSize = 40 + samplesLength;
343
    this.fmt.chunkSize = 20;
344
    this.fmt.cbSize = 2;
345
    this.fmt.validBitsPerSample = 8;
346
    this.fact = {
347
      chunkId: 'fact',
348
      chunkSize: 4,
349
      dwSampleLength: samplesLength
350
    };
351
  }
352
353
  /**
354
   * Set the string code of the bit depth based on the 'fmt ' chunk.
355
   * @private
356
   */
357
  bitDepthFromFmt_() {
358
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
359
      this.bitDepth = '32f';
360
    } else if (this.fmt.audioFormat === 6) {
361
      this.bitDepth = '8a';
362
    } else if (this.fmt.audioFormat === 7) {
363
      this.bitDepth = '8m';
364
    } else {
365
      this.bitDepth = this.fmt.bitsPerSample.toString();
366
    }
367
  }
368
369
  /**
370
   * Validate the bit depth.
371
   * @return {boolean} True is the bit depth is valid.
372
   * @throws {Error} If bit depth is invalid.
373
   * @private
374
   */
375
  validateBitDepth_() {
376
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
377
      if (parseInt(this.bitDepth, 10) > 8 &&
378
          parseInt(this.bitDepth, 10) < 54) {
379
        return true;
380
      }
381
      throw new Error('Invalid bit depth.');
382
    }
383
    return true;
384
  }
385
386
  /**
387
   * Update the type definition used to read and write the samples.
388
   * @private
389
   */
390
  updateDataType_() {
391
    this.dataType = {
392
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
393
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
394
      signed: this.bitDepth != '8',
395
      be: this.container == 'RIFX'
396
    };
397
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
398
      this.dataType.bits = 8;
399
      this.dataType.signed = false;
400
    }
401
  }
402
403
  /**
404
   * Validate the header of the file.
405
   * @throws {Error} If bit depth is invalid.
406
   * @throws {Error} If the number of channels is invalid.
407
   * @throws {Error} If the sample rate is invalid.
408
   * @ignore
409
   * @private
410
   */
411
  validateWavHeader_() {
412
    this.validateBitDepth_();
413
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
414
      throw new Error('Invalid number of channels.');
415
    }
416
    if (!validateSampleRate(
417
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
418
      throw new Error('Invalid sample rate.');
419
    }
420
  }
421
}
422